Daily Currency Price updates for the Synchronized Products
When using WP Global Cart to synchronize products between WooCommerce shops that use different currencies, the same product may need to have a different price on each shop.
For example, a product may have an original price of €100 EUR on the main shop, while another synchronized shop uses USD. The product on the second shop should have its price automatically converted to the local currency.
The Daily Currency Price Synchronization code provides a simple way to keep these prices updated automatically.
How It Works
When a product is synchronized to another shop using the WP Global Cart Product Synchronisation functionality, two additional pieces of information are sent along with the product:
- Origin product currency – the currency used by the original shop.
- Origin product price – the original product price in that currency.
These values are stored with the synchronized product as:
_origin_product_currency
_origin_product_price
The destination shop then uses this information to calculate the correct price in its own WooCommerce currency.
For example:
Origin shop:
Price: 100 EUR
Currency: EUR
Destination shop:
Currency: USD
Price: automatically converted from 100 EUR
The conversion is performed using the lightweight WOOGC_Currency_Converter class, which uses European Central Bank (ECB) exchange rates. This means there is no need to install an additional currency converter or multi-currency plugin for this functionality. [article here]
Automatic Daily Updates
The price synchronization runs automatically once per day ( or custom-defined). It takes the stored origin price and converts it to the current currency of the local WooCommerce shop.
This is important because the conversion is always performed from the original price, rather than converting the previously converted price. As exchange rates change, the local product price can therefore be updated accordingly without accumulating conversion errors.
The solution works with WooCommerce simple products, variable products, and product variations, allowing synchronized product catalogs to maintain appropriate local prices.
Ideal for Multi-Shop Stores
This feature is particularly useful when a WP Global Cart network contains shops operating with different currencies. Each shop can maintain its own local WooCommerce currency while synchronized products automatically receive prices converted from their original shop currency.
This provides a lightweight solution for maintaining consistent and up-to-date product pricing across a multi-currency WooCommerce network.
The code should be placed inside a custom file in the /wp-content/mu-plugins/ folder.
/**
* Scheduled currency price sync for WooCommerce products.
*
* Runs on a recurring WP-Cron schedule (default: once per day) and updates
* the price of every product — simple, variable (per variation), external,
* grouped, etc. — that carries both the '_origin_product_currency' and
* '_origin_product_price' meta keys, converting the stored origin price
* into the shop's current currency via WOOGC_Currency_Converter::convert().
*
* Products/variations missing EITHER meta key are skipped and the loop
* continues to the next one.
*
* USAGE:
* Paste this into a custom/mu-plugin file, or into your theme's
* functions.php. Requires the plugin that provides WOOGC_Currency_Converter
* to be active.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
define( 'CUSTOM_PRICE_SYNC_HOOK', 'custom_price_sync_cron_hook' );
add_filter( 'woogc/ps/synchronize_product/origin_product/meta_data', 'custom_woogc_add_site_currency', 10, 1 );
function custom_woogc_add_site_currency( $args )
{
$product = wc_get_product( $args[ 'origin_product_id' ] );
$args['product_meta']['_origin_product_currency'][] = get_option( 'woocommerce_currency' );
$args['product_meta']['_origin_product_price'][] = $product->get_price();
return $args;
}
/**
*
* Change the interval via the 'custom_price_sync_interval_seconds' filter,
* e.g. add_filter( 'custom_price_sync_interval_seconds', fn() => HOUR_IN_SECONDS * 6 );
*
* NOTE: if you change this after the event is already scheduled, run
* custom_price_sync_unschedule_event() once (or deactivate/reactivate)
* so WordPress picks up the new interval — WP doesn't reschedule an
* already-queued event automatically.
*/
add_filter( 'cron_schedules', 'custom_price_sync_register_interval' );
function custom_price_sync_register_interval( $schedules ) {
$schedules['custom_price_sync_interval'] = array(
'interval' => apply_filters( 'custom_price_sync_interval_seconds', DAY_IN_SECONDS ),
'display' => __( 'Custom Price Sync Interval', 'custom-price-sync' ),
);
return $schedules;
}
/**
* Schedule the event if it isn't already scheduled.
*/
add_action( 'init', 'custom_price_sync_schedule_event' );
function custom_price_sync_schedule_event() {
if ( ! wp_next_scheduled( CUSTOM_PRICE_SYNC_HOOK ) ) {
wp_schedule_event( time(), 'custom_price_sync_interval', CUSTOM_PRICE_SYNC_HOOK );
}
}
/**
* Unschedule helper — call this from a plugin deactivation hook if you turn
* this into its own plugin file:
* register_deactivation_hook( __FILE__, 'custom_price_sync_unschedule_event' );
*/
function custom_price_sync_unschedule_event() {
$timestamp = wp_next_scheduled( CUSTOM_PRICE_SYNC_HOOK );
if ( $timestamp ) {
wp_unschedule_event( $timestamp, CUSTOM_PRICE_SYNC_HOOK );
}
}
/**
* The cron callback: loops through every product in batches and updates
* prices for any product/variation carrying the origin currency/price meta.
*/
add_action( CUSTOM_PRICE_SYNC_HOOK, 'custom_price_sync_run' );
function custom_price_sync_run() {
if ( ! class_exists( 'WOOGC_Currency_Converter' ) ) {
custom_price_sync_log( 'WOOGC_Currency_Converter class not found. Aborting sync.' );
return;
}
$batch_size = apply_filters( 'custom_price_sync_batch_size', 300 );
$paged = 1;
do {
$query = new WP_Query( array(
'post_type' => 'product',
'post_status' => 'publish',
'posts_per_page' => $batch_size,
'paged' => $paged,
'fields' => 'ids',
'orderby' => 'ID',
'order' => 'ASC',
'no_found_rows' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
) );
if ( empty( $query->posts ) ) {
break;
}
foreach ( $query->posts as $product_id ) {
$product = wc_get_product( $product_id );
if ( ! $product ) {
continue;
}
if ( $product->is_type( 'variable' ) ) {
$updated_any_variation = false;
foreach ( $product->get_children() as $variation_id ) {
$variation = wc_get_product( $variation_id );
if ( ! $variation ) {
continue;
}
if ( custom_price_sync_maybe_update_product( $variation ) ) {
$updated_any_variation = true;
}
}
// Refresh the parent's cached price range after variations change.
if ( $updated_any_variation && class_exists( 'WC_Product_Variable' ) ) {
WC_Product_Variable::sync( $product_id );
wc_delete_product_transients( $product_id );
}
} else {
// Simple, external, grouped, etc.
custom_price_sync_maybe_update_product( $product );
}
}
$paged++;
} while ( true );
custom_price_sync_log( 'Price sync run completed.' );
}
/**
* Updates a single product/variation's price from its origin currency/price
* meta, if both metas exist. Returns true if the price was updated.
*
* @param WC_Product $product
* @return bool
*/
function custom_price_sync_maybe_update_product( $product ) {
$product_id = $product->get_id();
// Skip products that don't have BOTH meta keys set (this is the correct
// existence check — get_post_meta()/get_meta() return '' both when a
// meta is missing AND when it's set to an empty string).
if ( ! metadata_exists( 'post', $product_id, '_origin_product_currency' )
|| ! metadata_exists( 'post', $product_id, '_origin_product_price' ) ) {
return false;
}
$origin_currency = $product->get_meta( '_origin_product_currency', true );
$origin_price = $product->get_meta( '_origin_product_price', true );
if ( '' === $origin_currency || '' === $origin_price || ! is_numeric( $origin_price ) ) {
custom_price_sync_log( sprintf( 'Product #%d has invalid origin meta, skipping.', $product_id ) );
return false;
}
$shop_currency = get_option( 'woocommerce_currency' );
// Nothing to convert if origin currency already matches the shop currency.
if ( $origin_currency === $shop_currency ) {
$new_price = floatval( $origin_price );
} else {
$new_price = WOOGC_Currency_Converter::convert(
floatval( $origin_price ),
$origin_currency,
$shop_currency
);
if ( false === $new_price ) {
custom_price_sync_log( sprintf(
'Currency conversion failed for product #%d (%s -> %s), skipping.',
$product_id,
$origin_currency,
$shop_currency
) );
return false;
}
}
$new_price = wc_format_decimal( $new_price, wc_get_price_decimals() );
$old_regular_price = $product->get_regular_price();
$old_sale_price = $product->get_sale_price();
$product->set_regular_price( $new_price );
// If there's no active sale price, keep the active price in sync too.
// (If a sale price exists, we leave it as-is rather than guessing a new one.)
if ( '' === $old_sale_price ) {
$product->set_price( $new_price );
}
$product->save();
custom_price_sync_log( sprintf(
'Product #%d price updated: %s %s -> %s %s (regular price %s -> %s).',
$product_id,
$origin_price,
$origin_currency,
$new_price,
$shop_currency,
$old_regular_price,
$new_price
) );
return true;
}
/**
* Simple logging helper — writes to WooCommerce > Status > Logs
* (source: custom-price-sync). Disable with:
* add_filter( 'custom_price_sync_enable_logging', '__return_false' );
*/
function custom_price_sync_log( $message ) {
if ( ! apply_filters( 'custom_price_sync_enable_logging', true ) ) {
return;
}
if ( function_exists( 'wc_get_logger' ) ) {
wc_get_logger()->info( $message, array( 'source' => 'custom-price-sync' ) );
}
}

No Comments